Skip to Content
🎉 Welcome to JS World!!! 🎉

11장 원시 값과 객체의 비교

  • JS가 제공하는 데이터 타입은 원시 타입 primitive type객체 타입 object/reference type이 있다.
원시 타입 값객체 타입 값
number, string, boolean, null, undefined, symbolObject
변경 불가능한 값변경 가능한 값
변수(할당된 메모리 공간)에 실제 값 저장변수(할당된 메모리 공간)에 참조 값 저장
값에 의한 전달 : 다른 변수 할당 시 원시 값 복사되어 전달참조에 의한 전달 : 다른 변수 할당 시 참조 값이 복사되어 전달

11.1 원시 값

11.1.1 변경 불가능한 값

  • 한 번 생성된 원시 값은 읽기 전용 값으로 변경할 수 없다.

  • 변수와 값 구분해서 생각하기

    • 변수 : 하나의 값을 저장하기 위해 확보한 메모리 공간 자체 혹은 이 메모리 공간을 식별하기 위한 이름
    • 값 : 변수에 저장된 데이터, 표현식이 평가되어 생성된 결과
  • 변경 불가능하다는 것은 변수가 아니라 값에 대한 진술

    • 변수는 재할당을 통해 변수 값을 교체할 수 있다.
    • 상수는 재할당이 금지된 변수이다.
    • 단, const 키워드를 사용해 선언한 변수에 할당된 객체의 프로퍼티는 변경할 수 있다.
  • 4.5절에서 보았듯 원시 값을 할당한 변수에 새로운 원시 값을 재할당하면, 새로운 메모리 공간을 확보하고 재할당한 원시 값을 저장하고, 변수가 가리키는 주소 공간을 바꿔준다.

    • 메모리 공간 주소를 바꾸는 이유가 바로 변수에 할당된 원시 값이 변경 불가능하기 때문이다.
    • 만약 원시 값이 변경 가능했다면 주소를 바꿀 필요 없이, 원시 값 자체를 변경하면 그만이다.
    • 값의 이러한 특성을 불변성(immutability)이라 한다.
Note

불변성
불변성을 갖는 원시 값을 할당한 변수는 재할당 이외에 변수 값을 변경할 수 있는 방법이 없다.
재할당을 통해서만 바꾸도록 강제하는 것은 값이 변경되는 상태 추적을 쉽게 하고 예기치 못한 변수 값 변경을 막기 위함이다.

11.1.2 문자열과 불변성

  • 6.9.1절에서 보았듯 원시 값 저장 이전에 값의 타입에 따라 메모리 크기를 정하고 할당해야 한다.
  • ECMAScript에는 문자열 타입(2 byte), 숫자 타입(8 byte) 외의 타입은 크기를 명확히 규정하고 있지 않아 브라우저 엔진 구현에 따라 원시 타입의 크기가 다를 수 있다.
Note

JS 원시 값 문자열의 독특한 특징

  • 문자열은 0개 이상의 문자(character)로 이뤄진 집합이다.
  • 1개의 문자는 2 byte의 메모리 공간에 저장된다.
  • 문자열은 몇 개의 문자로 이뤄졌냐에 따라 필요한 메모리 공간의 크기가 결정된다.
  • 숫자의 경우는 1이든 10000000 이든 동일한 8byte가 할당된다.
  • C의 경우 문자열 타입은 없다. JAVA에선 문자열을 String 객체로 처리한다. 그러나 JS에선 원시 타입 문자열이 존재하는 것이 장점이다. 원시 타입이므로 생성 이후 변경 불가능하다.
  • 문자열은 유사 배열 객체이면서 iterable이므로 배열과 유사한 방식으로 각 문자에 접근 가능하다.
Note

유사 배열 객체
배열처럼 인덱스로 프로퍼티 값에 접근할 수 있고, length 프로퍼티를 갖는 객체를 말한다.
반복문으로 순회 가능하다.

  • 원시 값을 객체처럼 사용하면 원시 값을 감싸는 래퍼 객체로 자동 변환된다. (21.3절)

  • 문자열은 원시 값이므로 변경할 수 없다. 원시 값의 신뢰성을 보장할 수 있다. 그러나 에러는 발생하지 않는다.

var str = "javascript"; str[0] = "S"; console.log(str); // javascript
  • 변수에 새롭게 문자열을 할당하는 것은 가능하다.

11.1.3 값에 의한 전달

var score = 80; var copy = score; console.log(score); // 80 console.log(copy); // 80 score = 100; console.log(score); // 100 console.log(copy); // ?? -> 80
  • 변수에 변수를 할당했을 때 무엇이 어떻게 전달되는가?

  • 이 예시에서 copy 변수에 score를 할당하는 순간 새로운 값이 생성되어 할당된다.

  • 원시 값을 갖는 변수를 할당하면 할당받는 변수에는 원시 값이 복사되어 전달된다. 이것이 값에 의한 전달이다.

    • copy외 score 변수는 동일한 숫자 값 80을 갖고, score === copy 의 결과도 true이다. 하지만 실제론 다른 메모리 공간에 저장된 별개의 값이다.
  • ECMAScript 사양에는 변수를 통해 메모리를 어떻게 관리하는지는 명확히 정의되어 있지 않으므로, 엔진 구현에 따라 내부 동작은 달라질 수 있다.

    • 변수에 원시 값을 갖는 변수를 할당하는 순간에 원시 값을 복사해서 새로운 메모리에 올릴 수 있다.
    • 변수에 원시 값을 갖는 변수를 할당하는 순간에는 같은 원시 값을 참조하다가, 어느 한 쪽의 변수에 다른 값이 재할당되는 시점에 새로운 메모리 공간을 가리키도록 옮겨가게 구현할 수도 있다. (Python 작동 방식)
🚫
Caution

“값에 의한 전달” 용어 주의점

  • “값에 의한 전달” 용어도 ECMAScript 사양에는 없다.
  • “공유에 의한 전달” 이라고 표현하는 경우도 있다.
  • 엄밀히 표현하면 JS의 변수에는 값이 전달되는 것이 아니라 메모리 주소가 전달된다. 변수와 같은 식별자는 값이 아니라 메모리 주소를 기억하고 있다.
    • 식별자는 메모리 주소에 붙인 이름이다.
  • 이 때문에 C언어와 달리 JS에서 “값에 의한 전달”이라는 용어를 사용하면 오해의 여지가 있을 수 있다.
    • JS에서 “값에 의한 전달”도 사실은 메모리 주소를 전달하며, 변수에 접근하면 이 주소를 통해 실제 값을 참조 가능하다.
  • 중요한 결론은 원시 값을 갖는 변수의 경우 다른 곳에 할당되면 복사되어 다른 메모리 공간에 재할당되므로, 서로 간섭할 수 없다.

11.2 객체

  • 객체는 프로퍼티 개수가 정해져 있지 않고, 동적으로 추가 및 삭제 가능하다. 프로퍼티 값에 대한 제약도 없다. 따라서 객체는 원시 값과 달리 확보해야 할 메모리 공간의 크기를 사전에 정해둘 수 없다.
  • 객체는 복합 자료구조이므로 객체를 관리하는 방식이 원시 값에 비해 복잡하고, 구현 방식도 엔진마다 다를 수 있다.
Note

JavaScript 객체의 관리 방식

  • JS 객체는 프로퍼티 키를 index로 사용하는 해시 테이블이라 생각할 수 있다. 대부분 JS 엔진은 해시 테이블과 유사하지만 더 나은 성능을 가진 방법 (V8에선 hidden class 방식)으로 객체를 구현한다.
  • JAVA, C++ 같은 클래스 기반 객체지향 언어는 사전에 정의된 클래스를 기반으로 객체(인스턴스)를 생성한다. 객체 생성 이전에 이미 프로퍼티와 메서드가 정해져 있다.
  • 반면 JS는 객체 생성 이후여도 동적으로 프로퍼티와 메서드를 추가할 수 있다. 사용하기엔 편리하지만 성능 면에선 이론적으로 비용이 더 많이 든다.

11.2.1 변경 가능한 값

  • 객체(참조) 타입의 값, 객체는 변경 가능한 값이다.
  • 객체를 할당받은 변수의 메모리에는, 생성된 객체가 실제로 저장된 메모리 공간의 주소 값(참조 값)만 저장되어 있다. ![[./11-7.png]]
  • 객체는 변경 가능한 값이므로 재할당 없이 프로퍼티를 동적으로 추가할 수 있고, 갱신할 수 있고, 삭제할 수 있다.
  • 객체는 원시 값과 다르게 여러 개의 식별자가 하나의 동일한 객체(주소)를 공유할 수 있다.
Note

얕은 복사와 깊은 복사

  • 객체의 모든 프로퍼티와 값을 복사(deep-copy)하는 것은 비용이 많이 들어서 객체는 기본적으로 얕은 복사가 된다.

  • 객체를 프로퍼티 값으로 갖는 객체의 경우 얕은 복사는 한 단계까지만 복사하는 것을 말하고, 깊은 복사는 객체에 중첩된 객체까지 모두 복사하는 것을 말한다.

  • 얕은 복사와 깊은 복사로 생성된 객체는 원본과 다른 객체이다. 원본과 복사본은 참조 값이 다른 별개의 객체이다.

    • 하지만 얕은 복사는 객체가 중첩된 경우 내부 객체의 참조 값만 복사하고, 깊은 복사는 중첩된 객체를 모두 복사한다.
  • 단순히 객체가 할당된 변수를 다른 변수에 할당하는 것을 얕은 복사, 원시 값이 할당된 변수를 다른 변수에 할당하는 것을 깊은 복사라 부르기도 한다.

const o = { x: { y: 1 } }; //얕은 복사 const c1 = { ...o }; console.log(c1 === o); //false console.log(c1.x === o.x); //true //깊은 복사 - lodash의 cloneDeep 이용 const _ = require("lodash"); const c2 = _.cloneDeep(o); console.log(c1 === o); //false console.log(c1.x === o.x); //false

11.2.2 참조에 의한 전달

  • 여러 식별자가 참조 주소값만 복사해서 하나의 객체를 공유할 수 있다는 것의 의미

  • 포인터 개념과 비슷하게, 한 쪽에서 객체의 프로퍼티를 변경하면 다른 곳에도 동일하게 적용된다.

  • 결국 “값에 의한 전달”과 “참조에 의한 전달”은 식별자가 기억하는 메모리 공간에 저장되어 있는 값을 복사해서 전달한다는 면에서 동일하다.

    • 다만 식별자가 기억하는 메모리 공간 안의 값이 원시 값이냐 참조 값이냐의 차이가 있을 뿐이다.
  • 따라서 Javascript에서는 “참조에 의한 전달”은 존재하지 않고, “값에 의한 전달”만이 존재한다고 말할 수 있다.

    • 다만 이 용어는 다른 언어에서 가져온 것이고, JavaScript의 이 동작을 설명하는 공식적인 용어가 없기 때문에 “공유에 의한 전달”으로 표현하기도 한다.
    • 이 책에서는 전달되는 값의 종류가 원시 값인지 참조 값인지 구별하고 강조하는 의미에서 “값에 의한 전달”과 “참조에 의한 전달”로 구분하여 부른다.
    • JavaScript에서는 포인터 pointer가 존재하지 않기 때문에 pointer가 존재하는 언어의 “참조에 의한 전달” 의미와는 명확이 일치하지 않는다는 점을 주의해야 한다.
Quiz
var person1 = { name: "Lee", }; var person2 = { name: "Lee", }; console.log(person1 === person2); // 1 console.log(person1.name === person2.name); // 2
  • 객체를 할당한 변수는 참조 값을 가지고 있고, 원시 값을 할당한 변수는 원시 값 자체를 가지고 있다. 따라서 일치 비교 연산자로 객체 변수를 비교하면 참조 값을 비교하고, 원시 값 변수를 비교하면 값 자체를 비교한다.

[!note] === 일치 비교 연산자

  • 일치 비교 연산자는 변수에 저장되어 있는 값을 타입 변환하지 않고 비교한다.
  • 객체 리터럴은 평가될 때마다 객체를 생성한다. 따라서 person1과 person2의 객체는 다른 메모리 공간에 저장된 객체이다. 1번은 false이다.
  • 하지만 property 값을 참조하는 person1.name과 person2.name은 원시 값이 들어있다. 따라서 원시 값 ‘Lee’로 평가해서 비교하므로 2번은 true이다.
Last updated on